#include <assert.h>
#include <string>
#include <Windows.h>
#include <dokan.h>
#include "cpfs.h"
#include "logging.h"
#include "common.h"

using std::string;

static const CpfsPerms defaultCpfsPerms = 0777;
static const CpfsUid defaultCpfsUid = 65534; // nobody
static const CpfsGid defaultCpfsGid = 65534; // nogroup

/* OPERATION UTILITIES, CONVERSION ETC. */

// seconds between 1/1/1601 and 1/1/1970
static int64_t const secs_between_epochs = 11644473600;
static uint64_t const ten_e7 = 10000000; // 100ns

static void split_to_DWORDs(LONGLONG in, DWORD &low, DWORD &high)
{
    LARGE_INTEGER largeInt;
    largeInt.QuadPart = in;
    low = largeInt.LowPart;
    high = largeInt.HighPart;
}

static void FILETIME_to_CpfsTime(FILETIME const *in, CpfsTime *out)
{
    LARGE_INTEGER largeInt = {in->dwLowDateTime, in->dwHighDateTime};
    out->ns = (largeInt.QuadPart % ten_e7) * 100;
    out->sec = largeInt.QuadPart / ten_e7 - secs_between_epochs;
}

static void CpfsTime_to_FILETIME(CpfsTime const *in, FILETIME *out)
{
    LONGLONG longLong;
    longLong = ten_e7 * (in->sec + secs_between_epochs);
    longLong += in->ns / 100;
    split_to_DWORDs(longLong, out->dwLowDateTime, out->dwHighDateTime);
}

static string cpfs_path_from_dokan_filename(LPCWSTR dokanFileName)
{
    int outputSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, dokanFileName, -1, NULL, 0, NULL, NULL);
    assert(outputSize != 0);
    string retval(outputSize, '\0');
    retval.resize(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, dokanFileName, -1, &retval[0], outputSize, NULL, NULL));
    for (size_t pos = 0; (pos = retval.find("\\", pos)) != string::npos; pos += 1) {
        retval.replace(pos, 1, "/");
    }
    return retval;
}

static DWORD win32_error_value_from_cpfs_errno(int cpfsErrno)
{
    switch (cpfsErrno)
    {
    case EINVAL:
        return ERROR_INVALID_PARAMETER;
    case ENOENT:
        return ERROR_FILE_NOT_FOUND;
    case ENFILE:
        return ERROR_TOO_MANY_OPEN_FILES;
    case ENOTDIR:
        return ERROR_DIRECTORY;
    default:
        LOGF(Error, "Cpfs errno was not mapped: [%d] %s", cpfsErrno, strerror(cpfsErrno));
        return cpfsErrno;
    }
}

static int dokan_callback_retval_from_cpfs_errno()
{
    assert(cpfs_errno_isset());
    int retval = -static_cast<int>(win32_error_value_from_cpfs_errno(cpfs_errno_get()));
    assert(retval < 0);
    return retval;
}

static void init_dokan_context(DOKAN_FILE_INFO &dfi, int fd)
{
    assert(!dfi.Context);
    assert(fd != -1);
    dfi.Context = fd + 1;
}

static int cpfs_fd_init(DOKAN_FILE_INFO &dfi)
{
    if (dfi.Context) {
        return boost::numeric_cast<unsigned int>(dfi.Context - 1);
    } else {
        assert(false);
        return -1;
    }
}

static DWORD FILE_ATTRIBUTES_init(CpfsStat const &cpfsStat, char const *name)
{
    DWORD retval = 0;
    switch (cpfsStat.type) {
    case CPFS_ITYP_REG:
        retval |= FILE_ATTRIBUTE_NORMAL;
        break;
    case CPFS_ITYP_DIR:
        retval |= FILE_ATTRIBUTE_DIRECTORY;
        break;
    case CPFS_ITYP_LNK:
        retval |= FILE_ATTRIBUTE_REPARSE_POINT;
        break;
    default:
        retval |= FILE_ATTRIBUTE_DEVICE; // maybe system?
        break;
    }
    retval |= FILE_ATTRIBUTE_READONLY;
    if (    cpfsStat.perms & 02
            || cpfsStat.gid == defaultCpfsGid && cpfsStat.perms & 020
            || cpfsStat.uid == defaultCpfsUid && cpfsStat.perms & 0200) {
        retval &= ~FILE_ATTRIBUTE_READONLY;
    }
#if 0
    char const *lastSep(strrchr(name, '/'));
    if (!lastSep) {
        lastSep = name - 1;
    }
    if (lastSep[1] == '.') {
        retval |= FILE_ATTRIBUTE_HIDDEN;
    }
#endif
    return retval;
}

static void init_BY_HANDLE_FILE_INFORMATION(BY_HANDLE_FILE_INFORMATION &to, CpfsStat const &cpfsStat, DWORD volumeSerialNumber, char const *name)
{
    to.dwFileAttributes = FILE_ATTRIBUTES_init(cpfsStat, name);
    CpfsTime_to_FILETIME(&cpfsStat.ctime, &to.ftCreationTime);
    CpfsTime_to_FILETIME(&cpfsStat.atime, &to.ftLastAccessTime);
    CpfsTime_to_FILETIME(&cpfsStat.mtime, &to.ftLastWriteTime);
    split_to_DWORDs(cpfsStat.size, to.nFileSizeLow, to.nFileSizeHigh);
    to.nNumberOfLinks = cpfsStat.nlink;
    split_to_DWORDs(cpfsStat.ino, to.nFileIndexLow, to.nFileIndexHigh);
    to.dwVolumeSerialNumber = volumeSerialNumber;
}

static void init_WIN32_FIND_DATAW(WIN32_FIND_DATAW &win32FindData, CpfsStat const &cpfsStat, char const *cpfsFileName)
{
    win32FindData.dwFileAttributes = FILE_ATTRIBUTES_init(cpfsStat, cpfsFileName);
    CpfsTime_to_FILETIME(&cpfsStat.ctime, &win32FindData.ftCreationTime);
    CpfsTime_to_FILETIME(&cpfsStat.atime, &win32FindData.ftLastAccessTime);
    CpfsTime_to_FILETIME(&cpfsStat.mtime, &win32FindData.ftLastWriteTime);
    split_to_DWORDs(cpfsStat.size, win32FindData.nFileSizeLow, win32FindData.nFileSizeHigh);
    if (cpfsStat.type == CPFS_ITYP_LNK) {
        win32FindData.dwReserved0 = IO_REPARSE_TAG_SYMLINK;
    }
    int d = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, cpfsFileName, -1, win32FindData.cFileName, sizeof(win32FindData.cFileName));
    assert(0 < d && d <= MAX_PATH);
    wcsncpy_s(win32FindData.cAlternateFileName, L"", 14);
}

/* CPFS_DOKAN CALLBACKS */

#define CPFS_DOKAN_CONTEXT reinterpret_cast<CpfsDokanGlobalContext *>(dokanFileInfo->DokanOptions->GlobalContext)
#define cpfs CPFS_DOKAN_CONTEXT->cpfsInstance

static int DOKAN_CALLBACK
cpfs_dokan_create_file(
	    LPCWSTR		        fileName,
	    DWORD				accessMode,
	    DWORD				shareMode,
	    DWORD				creationDisposition,
	    DWORD				flagsAndAttributes,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    assert(dokanFileInfo->Context == 0);
    assert(!dokanFileInfo->IsDirectory);
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    int retval = 0;
    int fd = cpfs_open(cpfs, cpfsPath.c_str());
    if (-1 != fd) {
        switch (creationDisposition) {
        case CREATE_ALWAYS:
            verify(cpfs_ftruncate(cpfs, fd, 0));
            retval = ERROR_ALREADY_EXISTS;
            break;
        case CREATE_NEW:
            verify(cpfs_close(cpfs, fd));
            return -ERROR_FILE_EXISTS;
        case OPEN_ALWAYS:
            retval = ERROR_ALREADY_EXISTS;
            break;
        case TRUNCATE_EXISTING:
            assert(accessMode & GENERIC_WRITE);
            verify(cpfs_ftruncate(cpfs, fd, 0));
            break;
        }
    } else if (cpfs_errno_get() == ENOENT) {
        switch (creationDisposition) {
        case OPEN_EXISTING:
        case TRUNCATE_EXISTING:
            return -ERROR_FILE_NOT_FOUND;
        default:
            fd = cpfs_create(cpfs, cpfsPath.c_str(), CPFS_ITYP_REG, defaultCpfsPerms, defaultCpfsUid, defaultCpfsGid);
        }
    }
    if (fd != -1) {
        CpfsStat cpfsStat = {};
        if (!cpfs_fstat(cpfs, fd, &cpfsStat)) {
            return dokan_callback_retval_from_cpfs_errno();
        }
        if (cpfsStat.type == CPFS_ITYP_DIR) {
            dokanFileInfo->IsDirectory = 1;
        }
        init_dokan_context(*dokanFileInfo, fd);
        return retval;
    } else {
        return dokan_callback_retval_from_cpfs_errno();
    }
}

static int DOKAN_CALLBACK
cpfs_dokan_close_file(
	LPCWSTR					fileName,
	PDOKAN_FILE_INFO		dokanFileInfo)
{
    if (dokanFileInfo->Context) {
        if (cpfs_close(cpfs, cpfs_fd_init(*dokanFileInfo)))
            return 0;
        else
            return dokan_callback_retval_from_cpfs_errno();
    }
    else {
        return -ERROR_INVALID_PARAMETER;
    }
}

static int DOKAN_CALLBACK
cpfs_dokan_get_file_information(
	LPCWSTR							fileName,
	LPBY_HANDLE_FILE_INFORMATION	hFileInfo,
	PDOKAN_FILE_INFO				dokanFileInfo)
{
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    CpfsStat cpfsStat = {};
    if (!cpfs_stat(cpfs, cpfsPath.c_str(), &cpfsStat)) {
        return dokan_callback_retval_from_cpfs_errno();
    }
    init_BY_HANDLE_FILE_INFORMATION(*hFileInfo, cpfsStat, cpfs_params(cpfs)->serial, cpfsPath.c_str());
    return 0;
}

static int DOKAN_CALLBACK
cpfs_dokan_cleanup(
	LPCWSTR					fileName,
	PDOKAN_FILE_INFO		dokanFileInfo)
{
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    if (dokanFileInfo->DeleteOnClose) {
        if (dokanFileInfo->IsDirectory) {
            verify(cpfs_rmdir(cpfs, cpfsPath.c_str()));
        }
        else {
            verify(cpfs_unlink(cpfs, cpfsPath.c_str()));
        }
    }
    return 0;
}

static int DOKAN_CALLBACK
cpfs_dokan_open_directory(
	LPCWSTR					fileName,
	PDOKAN_FILE_INFO		dokanFileInfo)
{
    assert(!dokanFileInfo->Context);
    assert(dokanFileInfo->IsDirectory);
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    int fd = cpfs_opendir(cpfs, cpfsPath.c_str());
    if (fd == -1) {
        return dokan_callback_retval_from_cpfs_errno();
    }
    init_dokan_context(*dokanFileInfo, fd);
    return 0;
}

static int DOKAN_CALLBACK
cpfs_dokan_create_directory(
	LPCWSTR					fileName,
	PDOKAN_FILE_INFO		dokanFileInfo)
{
    assert(dokanFileInfo->IsDirectory);
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    if (cpfs_mknod(cpfs, cpfsPath.c_str(), CPFS_ITYP_DIR, 0777, defaultCpfsUid, defaultCpfsGid)) {
        return 0;
    } else {
        if (cpfs_errno_get() == EEXIST) {
            return -ERROR_ALREADY_EXISTS; // does this work here?
        } else {
            return dokan_callback_retval_from_cpfs_errno();
        }
    }
}

static int DOKAN_CALLBACK
cpfs_dokan_find_files(
	LPCWSTR				fileName,
	PFillFindData		fillFindData, // function pointer
	PDOKAN_FILE_INFO	dokanFileInfo)
{
    CpfsOff offset = -1; // skip . and ..
    while (true) {
        CpfsStat cpfsStat = {};
        string cpfsFileName(cpfs_params(cpfs)->namemax + 1, 0);
        if (!cpfs_readdir(cpfs, cpfs_fd_init(*dokanFileInfo), &offset, &cpfsFileName[0], &cpfsStat, true))
            return dokan_callback_retval_from_cpfs_errno();
        if (offset == 0)
            return 0;
        WIN32_FIND_DATAW win32FindData = {};
        init_WIN32_FIND_DATAW(win32FindData, cpfsStat, cpfsFileName.c_str());
        verify(0 == fillFindData(&win32FindData, dokanFileInfo));
    }
}

static int DOKAN_CALLBACK cpfs_dokan_get_volume_information(
	    LPWSTR volumeNameBuffer,
	    DWORD volumeNameSize,
	    LPDWORD volumeSerialNumber,
	    LPDWORD maximumComponentLength,
	    LPDWORD fileSystemFlags,
	    LPWSTR fileSystemNameBuffer,
	    DWORD fileSystemNameSize,
	    PDOKAN_FILE_INFO dokanFileInfo)
{
    verify(0 != MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, CPFS_DOKAN_CONTEXT->volumeName, -1, volumeNameBuffer, volumeNameSize));
    //*volumeSerialNumber = cpfs_params(cpfs)->serial;
    CpfsStatvfs cpfsStatvfs = {};
    verify(cpfs_statvfs(cpfs, &cpfsStatvfs));
    *maximumComponentLength = cpfsStatvfs.namemax;
    *fileSystemFlags = FILE_CASE_PRESERVED_NAMES | FILE_CASE_SENSITIVE_SEARCH | FILE_SUPPORTS_HARD_LINKS
        | FILE_SUPPORTS_REPARSE_POINTS;// | FILE_SUPPORTS_SPARSE_FILES | FILE_UNICODE_ON_DISK;
    char const fsName[] = "cpfs";
    verify(0 != MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, fsName, -1, fileSystemNameBuffer, fileSystemNameSize));
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_get_disk_free_space(
	    PULONGLONG freeBytesAvailable, // FreeBytesAvailable
	    PULONGLONG totalNumberOfBytes, // TotalNumberOfBytes
	    PULONGLONG totalNumberOfFreeBytes, // TotalNumberOfFreeBytes
	    PDOKAN_FILE_INFO dokanFileInfo)
{
    CpfsStatvfs cpfsStatvfs;
    if (!cpfs_statvfs(cpfs, &cpfsStatvfs))
        return dokan_callback_retval_from_cpfs_errno();
    *freeBytesAvailable = *totalNumberOfFreeBytes = cpfsStatvfs.bfree * cpfsStatvfs.bsize;
    *totalNumberOfBytes = cpfsStatvfs.blocks * cpfsStatvfs.bsize;
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_move_file(
	    LPCWSTR				fileName, // existing file name
	    LPCWSTR				newFileName,
	    BOOL				replaceIfExisting,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    string cpfsOldPath(cpfs_path_from_dokan_filename(fileName));
    string cpfsNewPath(cpfs_path_from_dokan_filename(newFileName));
    if (!replaceIfExisting) {
        CpfsStat cpfsStat;
        if (cpfs_stat(cpfs, cpfsNewPath.c_str(), &cpfsStat)) {
            return -ERROR_FILE_EXISTS;
        }
        else if (cpfs_errno_get() != ENOENT) {
            assert(false);
            return -1;
        }
    }
    if (cpfs_rename(cpfs, cpfsOldPath.c_str(), cpfsNewPath.c_str())) {
        return 0;
    }
    else {
        return dokan_callback_retval_from_cpfs_errno();
    }
}

static int DOKAN_CALLBACK cpfs_dokan_delete_directory(
	    LPCWSTR				fileName,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    //assert(!dokanFileInfo->Context);
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_set_file_attributes(
	    LPCWSTR				fileName,
	    DWORD				fileAttributes,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    if (fileAttributes & ~(FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_READONLY)) {
        return -ERROR_INVALID_PARAMETER;
    }
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    CpfsStat cpfsStat = {};
    if (!cpfs_stat(cpfs, cpfsPath.c_str(), &cpfsStat)) {
        return dokan_callback_retval_from_cpfs_errno();
    }
    CpfsPerms newPerms = cpfsStat.perms;
    if (fileAttributes & FILE_ATTRIBUTE_READONLY) {
        newPerms &= ~0222;
    }
    if (!cpfs_chmod(cpfs, cpfsPath.c_str(), newPerms)) {
        return dokan_callback_retval_from_cpfs_errno();
    }
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_set_file_time(
	    LPCWSTR fileName,
	    CONST FILETIME *creationTime,
	    CONST FILETIME *lastAccessTime,
	    CONST FILETIME *lastWriteTime,
	    PDOKAN_FILE_INFO dokanFileInfo)
{
    string cpfsPath(cpfs_path_from_dokan_filename(fileName));
    CpfsTime cpfsTimes[2];
    FILETIME_to_CpfsTime(lastAccessTime, &cpfsTimes[0]);
    FILETIME_to_CpfsTime(lastWriteTime, &cpfsTimes[1]);
    if (cpfs_utimens(cpfs, cpfsPath.c_str(), cpfsTimes)) {
        return 0;
    } else {
        return dokan_callback_retval_from_cpfs_errno();
    }
}

static int DOKAN_CALLBACK cpfs_dokan_lock_file(
	LPCWSTR, // FileName
	LONGLONG, // ByteOffset
	LONGLONG, // Length
	PDOKAN_FILE_INFO)
{
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_unlock_file(
	LPCWSTR, // FileName
	LONGLONG,// ByteOffset
	LONGLONG,// Length
	PDOKAN_FILE_INFO)
{
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_flush_file_buffers(
	    LPCWSTR fileName,
	    PDOKAN_FILE_INFO dokanFileInfo)
{
    verify(cpfs_fnop(cpfs, cpfs_fd_init(*dokanFileInfo)));
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_read_file(
	    LPCWSTR				fileName,
	    LPVOID				buffer,
	    DWORD				bufferLength,
	    LPDWORD				readLength,
	    LONGLONG			offset,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    CpfsSizeRet readret = cpfs_read(cpfs, cpfs_fd_init(*dokanFileInfo), buffer, bufferLength, offset);
    if (readret == -1) {
        return dokan_callback_retval_from_cpfs_errno();
    }
    *readLength = readret;
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_write_file(
	    LPCWSTR fileName,
	    LPCVOID buffer,
	    DWORD numberOfBytesToWrite,
	    LPDWORD numberOfBytesWritten,
	    LONGLONG offset,
	    PDOKAN_FILE_INFO dokanFileInfo)
{
    CpfsSizeRet written = cpfs_write(cpfs, cpfs_fd_init(*dokanFileInfo), buffer, numberOfBytesToWrite, offset);
    if (written == -1) {
        return dokan_callback_retval_from_cpfs_errno();
    }
    *numberOfBytesWritten = written;
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_delete_file(
	    LPCWSTR				fileName,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    return 0;
}

static int DOKAN_CALLBACK cpfs_dokan_set_end_of_file(
	    LPCWSTR				fileName,
	    LONGLONG			byteOffset,
	    PDOKAN_FILE_INFO	dokanFileInfo)
{
    if (cpfs_ftruncate(cpfs, cpfs_fd_init(*dokanFileInfo), byteOffset)) {
        return 0;
    } else {
        return dokan_callback_retval_from_cpfs_errno();
    }
}

CpfsDokanGlobalContext::~CpfsDokanGlobalContext()
{
    verify(cpfs_unload(this->cpfsInstance));
    //free(this->volumeName));
}

static int DOKAN_CALLBACK cpfs_dokan_unmount(
	    PDOKAN_FILE_INFO dokanFileInfo)
{
    delete CPFS_DOKAN_CONTEXT;
    dokanFileInfo->DokanOptions->GlobalContext = 0;
    return 0;
}

#undef cpfs
#undef CPFS_DOKAN_CONTEXT

DOKAN_OPERATIONS cpfsDokanOps = {
    cpfs_dokan_create_file,
    cpfs_dokan_open_directory,
    cpfs_dokan_create_directory,
    cpfs_dokan_cleanup,
    cpfs_dokan_close_file,
    cpfs_dokan_read_file,
    cpfs_dokan_write_file,
    cpfs_dokan_flush_file_buffers,
    cpfs_dokan_get_file_information,
    /* You should implement either FindFiles or FindFilesWithPattern */
    cpfs_dokan_find_files,
    NULL,
    cpfs_dokan_set_file_attributes,
    cpfs_dokan_set_file_time,
    cpfs_dokan_delete_file,
    cpfs_dokan_delete_directory,
    cpfs_dokan_move_file,
    cpfs_dokan_set_end_of_file,
    NULL,
    NULL,//cpfs_dokan_lock_file,
    NULL,// cpfs_dokan_unlock_file,
    cpfs_dokan_get_disk_free_space,
    NULL, //dokanOps.GetVolumeInformation = cpfs_dokan_get_volume_information,
    cpfs_dokan_unmount,
};